package org.beynet.docs.session;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.regex.Matcher;

import javax.security.auth.login.LoginException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.stream.StreamResult;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.log4j.Logger;
import org.beynet.docs.exceptions.GDocsServerError;
import org.beynet.docs.exceptions.UnexpectedStatusCode;
import org.beynet.docs.format.*;
import org.beynet.docs.http.GDocsHTTPConstants;
import org.beynet.docs.http.HttpHelper;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

public class SessionImpl implements Session {

    public SessionImpl() {
        uriByPath = new HashMap<String, URI>();
        uriContentByPath = new HashMap<String, URI>();
    }

    @Override
    public void doAuthentication(String login, char[] password) throws IOException, LoginException {
        HttpClient client = HttpHelper.getHttpClient();
        HttpPost loginRequest = new HttpPost(GDocsHTTPConstants.loging);
        loginRequest.addHeader("GData-Version", "3.0");
        List<NameValuePair> formparams = new ArrayList<NameValuePair>();
        formparams.add(new BasicNameValuePair("accountType", "GOOGLE"));
        formparams.add(new BasicNameValuePair("Email", login));
        formparams.add(new BasicNameValuePair("Passwd", new String(password)));
        formparams.add(new BasicNameValuePair("service", "writely"));
        formparams.add(new BasicNameValuePair("source", "fresnes-gsync-v1"));
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8");
        loginRequest.setEntity(entity);
        HttpResponse response = client.execute(loginRequest);
        HttpEntity repEntity = response.getEntity();
        InputStream is = (repEntity != null) ? repEntity.getContent() : null;
        try {
            if (response.getStatusLine().getStatusCode() == 200) {
                BufferedReader contentReader = new BufferedReader(new InputStreamReader(is));
                do {
                    String line = contentReader.readLine();
                    if (line.startsWith(AUTH_LINE)) {
                        auth = line.substring(AUTH_LINE.length());
                        break;
                    }
                } while (true);
            } else {
                throw new LoginException();
            }
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    @Override
    public String getAuthorizationToken() {
        return (auth);
    }

    @Override
    public String getAuthorizationHeaderValue() {
        return (new String("GoogleLogin Auth=" + auth));
    }

    protected DocumentListFeed readFeedIntoResponse(HttpResponse response) throws IOException, GDocsServerError {
        Header[] contents = response.getHeaders(HttpHelper.CONTENT_TYPE);
        String encoding = null;
        if (contents.length > 0) {
            Matcher m = HttpHelper.CHARSET_PATTERN.matcher(contents[0].getValue());
            if (m.matches()) {
                encoding = m.group(1);
            }
        }
        HttpEntity repEntity = response.getEntity();
        Document result;
        try {
            result = HttpHelper.parseHttpXmlStreamIntoDom(repEntity.getContent(), repEntity.getContentLength(), encoding);
        } catch (SAXException e) {
            throw new IOException("invalid xml received", e);
        }
        return (new DocumentListFeed(result));
    }

    @Override
    public DocumentListFeed getFeed(URI url) throws IOException, UnexpectedStatusCode, GDocsServerError {
        HttpClient client = HttpHelper.getHttpClient();
        HttpGet getRoot = new HttpGet(url);
        getRoot.addHeader(GDocsHTTPConstants.API_VERSION, "3.0");
        getRoot.addHeader("Authorization", getAuthorizationHeaderValue());
        HttpResponse response = client.execute(getRoot);
        int status = response.getStatusLine().getStatusCode();
        if (status == 200) {
            return (readFeedIntoResponse(response));
        } else {
            HttpHelper.releaseResource(response);
            if (status > 200 && status < 500) {
                // request error
                // -------------
                throw new UnexpectedStatusCode(status);
            } else {
                logger.error("error received from serveur (http code=" + status + ") - retry in 1 second");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                }
                // retrying once
                response = client.execute(getRoot);
                status = response.getStatusLine().getStatusCode();
                if (status == 200) {
                    return (readFeedIntoResponse(response));
                } else {
                    throw new UnexpectedStatusCode(status);
                }
            }
        }
    }

    @Override
    public DocumentListEntry getEntry(String url) throws IOException, GDocsServerError {
        HttpClient client = HttpHelper.getHttpClient();
        HttpGet getRoot = new HttpGet(url);
        getRoot.addHeader(GDocsHTTPConstants.API_VERSION, "3.0");
        getRoot.addHeader("Authorization", getAuthorizationHeaderValue());
        HttpResponse response = client.execute(getRoot);
        Header[] contents = response.getHeaders(HttpHelper.CONTENT_TYPE);
        String encoding = null;
        if (contents.length > 0) {
            Matcher m = HttpHelper.CHARSET_PATTERN.matcher(contents[0].getValue());
            if (m.matches()) {
                encoding = m.group(1);
            }
        }
        HttpEntity repEntity = response.getEntity();
        Document result;
        try {
            result = HttpHelper.parseHttpXmlStreamIntoDom(repEntity.getContent(), repEntity.getContentLength(), encoding);
        } catch (SAXException e) {
            throw new IOException("invalid xml received", e);
        }
        return (DocumentListEntry.makeFromEntryNode(result.getDocumentElement()));
    }

    @Override
    public DocumentListFeed getRootFolder(Session session) throws IOException, UnexpectedStatusCode, GDocsServerError {
        try {
            return (session.getFeed(new URI(GDocsHTTPConstants.LIST_MAIN)));
        } catch (URISyntaxException e) {
            throw new RuntimeException("error constructing URI - programmatic error ?", e);
        }
    }

    @Override
    public URI getDirectoryByPath(String path) throws IOException, UnexpectedStatusCode, GDocsServerError {
        if (path == null || path.length() == 0) {
            throw new IllegalArgumentException("path must not be null nor empty");
        }
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        StringTokenizer tokens = new StringTokenizer(path, "/");
        if (tokens.hasMoreTokens() == false) {
            return (null);
        }
        return (getDirectoryByPath(tokens, null, ""));
    }

    @Override
    public List<Collection> getCollectionsAtRootLevel() throws IOException, UnexpectedStatusCode, GDocsServerError {
        List<Collection> result = new ArrayList<Collection>();
        URI request = null;
        try {
            request = new URI(GDocsHTTPConstants.FOLDER_LEVEL1_QUERY);
        } catch (URISyntaxException e) {
            throw new RuntimeException("Invalid URI constructed - programmatic error ?", e);
        }
        DocumentListFeed tempFeed = getFeed(request);
        do {
            for (DocumentListEntry entry : tempFeed.getEntries()) {
                Collection collection = new Collection(entry.getTitle(), entry.getSelfLink().getURI(),entry.getContentURI());
                result.add(collection);
            }
            Link nextLink = tempFeed.getNextLink();
            if ((nextLink == null) || (tempFeed.getEntries().isEmpty())) {
                break;
            }
            tempFeed = getFeed(nextLink.getURI());

        } while (true);
        return (result);
    }

    @Override
    public void getCollectionChildsOfTypeCollection(URI collectionContent, List<Collection> result) throws IOException, UnexpectedStatusCode, GDocsServerError {
        DocumentListFeed tempFeed = getFeed(collectionContent);
        do {
            for (DocumentListEntry entry : tempFeed.getEntries()) {

                if (DocumentType.FOLDER.equals(entry.getType())) {
                    URI contentURI = entry.getContentURI();
                    if (contentURI == null) {
                        throw new GDocsServerError("found a collection without any content link :" + entry.getTitle());
                    }
                    Collection newCollection = new Collection(entry.getTitle(), entry.getSelfLink().getURI(),contentURI);
                    result.add(newCollection);
                }
            }
            Link nextLink = tempFeed.getNextLink();
            if ((nextLink == null) || (tempFeed.getEntries().isEmpty())) {
                break;
            }
            tempFeed = getFeed(nextLink.getURI());

        } while (true);
    }

    /**
     * internal recursive method - searching a directory by its name
     *
     * @param session
     * @param tokens
     * @param father
     * @return
     * @throws IOException
     */
    protected URI getDirectoryByPath(StringTokenizer tokens, URI fatherEntrySelfLink, String path) throws IOException, UnexpectedStatusCode, GDocsServerError {
        String title = tokens.nextToken();
        path = path + "/" + title;
        logger.debug("search for path " + path);

        URI contentURI = null;
        URI entrySelfURI = null;
        synchronized (uriByPath) {
            contentURI = uriContentByPath.get(path);
            entrySelfURI = uriByPath.get(path);
            if (contentURI != null) {
                logger.debug("path (" + path + ") in cache");

            }
        }
        if (entrySelfURI == null) {
            List<NameValuePair> params = new ArrayList<NameValuePair>();
            BasicNameValuePair titlePair = new BasicNameValuePair(GDocsHTTPConstants.QUERY_TITLE, title);
            BasicNameValuePair qExactPair = new BasicNameValuePair(GDocsHTTPConstants.QUERY_TITLE_EXACT, "true");
            params.add(titlePair);
            params.add(qExactPair);

            StringBuilder query = new StringBuilder(GDocsHTTPConstants.FOLDER_QUERY);
            query.append("?");
            query.append(URLEncodedUtils.format(params, "UTF-8"));
            DocumentListFeed tempFeed;
            try {
                tempFeed = getFeed(new URI(query.toString()));
            } catch (URISyntaxException e) {
                throw new RuntimeException("Invalid URI constructed - programmatic error ?", e);
            }
            DocumentListFeed allEntries = new DocumentListFeed();
            do {
                allEntries.getEntries().addAll(tempFeed.getEntries());
                Link nextLink = tempFeed.getNextLink();
                if ((nextLink == null) || (tempFeed.getEntries().isEmpty())) {
                    break;
                }
                try {
                    tempFeed = getFeed(nextLink.getURI());
                } catch (Exception e) {
                    // retry once before throwing exception
                    tempFeed = getFeed(nextLink.getURI());
                }
            } while (true);
            DocumentListEntry found = null;
            for (DocumentListEntry entry : allEntries.getEntries()) {
                if (fatherEntrySelfLink == null) {
                    if (entry.getParentLinks().isEmpty()) {
                        found = entry;
                        break;
                    }
                } else {
                    for (Link l : entry.getParentLinks()) {
                        if (l.getURI().equals(fatherEntrySelfLink)) {
                            found = entry;
                            break;
                        }
                    }
                }
            }
            if (found == null) {
                return (null);
            }
            entrySelfURI = found.getSelfLink().getURI();
            contentURI = found.getContentURI();
            synchronized (uriByPath) {
                uriByPath.put(path, entrySelfURI);
                uriContentByPath.put(path, contentURI);
            }
        }
        if (tokens.hasMoreTokens()) {
            return (getDirectoryByPath(tokens, entrySelfURI, path));
        }
        return (contentURI);
    }

    @Override
    public void createEmptyFolder(String newFolderName, URI destinationFolder) throws IOException, GDocsServerError, UnexpectedStatusCode {
        StringBuilder folder = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        folder.append("<entry xmlns=\"http://www.w3.org/2005/Atom\">\n");
        folder.append("<category scheme=\"http://schemas.google.com/g/2005#kind\" term=\"http://schemas.google.com/docs/2007#folder\"/>\n");
        folder.append("<title>$TITLE$</title>\n");
        folder.append("</entry>");
        String request = folder.toString();
        newFolderName = newFolderName.replaceAll("&", "&amp;");
        newFolderName = newFolderName.replaceAll("<", "&lt;");
        newFolderName = newFolderName.replaceAll(">", "&gt;");
        request = request.replace("$TITLE$", newFolderName);
        HttpPost postCreate = HttpHelper.createPostRequest(this, destinationFolder.toString());
        postCreate.addHeader("Content-Type", "application/atom+xml");
        postCreate.setEntity(new StringEntity(request, "UTF-8"));
        HttpClient client = HttpHelper.getHttpClient();
        HttpResponse response = client.execute(postCreate);
        HttpEntity responseEntity = response.getEntity();
        InputStream is = (responseEntity != null) ? responseEntity.getContent() : null;
        int status;
        try {
            status = response.getStatusLine().getStatusCode();
        } finally {
            if (is != null) {
                is.close();
            }
        }
        if (status >= 300 && status < 500) {
            throw new UnexpectedStatusCode(status);
        }
        if (status >= 500) {
            try {
                logger.error("Retry request after receiving status=" + status + " will retry in 1 sec.");
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
            }
            response = client.execute(postCreate);
            responseEntity = response.getEntity();
            is = (responseEntity != null) ? responseEntity.getContent() : null;
            try {
                status = response.getStatusLine().getStatusCode();
            } finally {
                if (is != null) {
                    is.close();
                }
            }
            if (status >= 300) {
                throw new UnexpectedStatusCode(status);
            }
        }
    }

    @Override
    public URI postNewFile(File file, String mimetype, URI destination) throws IOException, UnexpectedStatusCode {
        HttpClient client = HttpHelper.getHttpClient();
        HttpPost postQuery = HttpHelper.createPostRequest(this, destination.toString());
        postQuery.addHeader(GDocsHTTPConstants.SLUG, file.getName());
        postQuery.setEntity(new FileEntity(file, mimetype));
        HttpResponse resp = client.execute(postQuery);
        HttpEntity respEntity = resp.getEntity();
        InputStream is = (respEntity != null) ? resp.getEntity().getContent() : null;
        try {
            int code = resp.getStatusLine().getStatusCode();
            if (code != 201) {
                throw new UnexpectedStatusCode(resp.getStatusLine().getStatusCode());
            }
            Header[] location = resp.getHeaders(HttpHelper.LOCATION_HEADER);
            if (location.length > 0) {
                String located = location[0].getValue();
                try {
                    return (new URI(located));
                } catch (URISyntaxException e) {
                    throw new IOException("unvalid uri received");
                }
            }
            throw new IOException("no Location header received");
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }
    private String auth;
    private static final String AUTH_LINE = "Auth=";
    private final static Logger logger = Logger.getLogger(SessionImpl.class);
    private final Map<String, URI> uriByPath;
    private final Map<String, URI> uriContentByPath;
}
